<?php
// +----------------------------------------------------------------------
// | zhanshop-device / ssh.php    [ 2023/12/21 15:09 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2011~2023 zhangqiquan All rights reserved.
// +----------------------------------------------------------------------
// | Author: zhangqiquan <768617998@qq.com>
// +----------------------------------------------------------------------
declare (strict_types=1);

use Swoole\Coroutine\Http\Client;

class Ssh{
    /**
     * @var Client
     */
    protected $client = null;
    protected $connection = null;
    protected $shell = null;
    protected $replyPath = "/v1/ssh.response";

    public function __construct($client)
    {
        $this->client = $client;
    }

    public function auth($fd, $data){
        $this->connection = ssh2_connect($data['server'] ?? '127.0.0.1', intval($data['port'] ?? '22'));
        if($this->connection != false){
            if (ssh2_auth_password($this->connection, $data['user'] ?? 'root', $data['password'] ?? '123456')) {
                $this->shell = ssh2_shell($this->connection, 'xterm', null, 80, 24, SSH2_TERM_UNIT_CHARS);
                usleep(500000);
            }else{
                $respData = [
                    'uri' => $this->replyPath.'/'.$fd,
                    'header' => [],
                    'body' => [
                        "data" => "auth-error"
                    ]
                ];
                $this->client->push(json_encode($respData, JSON_UNESCAPED_SLASHES + JSON_UNESCAPED_UNICODE));
            }
        }else{
            $respData = [
                'uri' => $this->replyPath.'/'.$fd,
                'header' => [],
                'body' => [
                    "data" => "connect-error"
                ]
            ];
            $this->client->push(json_encode($respData, JSON_UNESCAPED_SLASHES + JSON_UNESCAPED_UNICODE));
        }
    }

    public function command($fd, $data){
        if($this->shell){
            fwrite($this->shell, $data['command']);
            usleep(100);
            while($line = fgets($this->shell)) {
                $respData = [
                    'uri' => $this->replyPath.'/'.$fd,
                    'header' => [],
                    'body' => [
                        "data" => mb_convert_encoding($line, "UTF-8")
                    ]
                ];
                $this->client->push(json_encode($respData, JSON_UNESCAPED_SLASHES + JSON_UNESCAPED_UNICODE));
            }
        }
    }

    public function refresh($fd, $data){
        if($this->shell){
            while($line = fgets($this->shell)) {
                $respData = [
                    'uri' => $this->replyPath.'/'.$fd,
                    'header' => [],
                    'body' => [
                        "data" => mb_convert_encoding($line, "UTF-8")
                    ]
                ];
                $this->client->push(json_encode($respData, JSON_UNESCAPED_SLASHES + JSON_UNESCAPED_UNICODE));
            }
        }
    }

    /**
     * 关闭
     * @return void
     */
    public function close(){
        if($this->shell){
            ssh2_disconnect($this->connection);
            fclose($this->shell);
        }
    }
}

class WebClient{
    protected $connPath = "/v1/ssh.connect";
    protected $config = [
        'type' => 1, // SWOOLE_SOCK_TCP
        'ip' => "127.0.0.1",
        'port' => "8201",
        'tofd' => 0,
        'fd' => 0,
        'time' => 0,
        'worker_id' => 0,
        'options' => [
            'timeout' => 5.0, //总超时，包括连接、发送、接收所有超时
            'connect_timeout' => 3.0,//连接超时，会覆盖第一个总的 timeout
            'write_timeout' => 3.0,//发送超时，会覆盖第一个总的 timeout
            'read_timeout' => 3.0,//接收超时，会覆盖第一个总的 timeout
        ],
    ];
    /**
     * @var Ssh
     */
    protected $ssh;

    /**
     * 构造方法
     * @param string $ip
     * @param string $port
     */
    public function __construct(string $type, string $ip, string $port, string $toFd)
    {
        $this->config['type'] = intval($type);
        $this->config['ip'] = $ip;
        $this->config['port'] = $port;
        $this->config['tofd'] = $toFd;
    }

    public function listen(){
        $this->config['time'] = microtime(true);
        // 这里要能使用IPV6
        if($this->config['type'] === SWOOLE_SOCK_TCP6){
            if (!filter_var($this->config['ip'], FILTER_VALIDATE_IP) ) {
                $addr = \Swoole\Coroutine\System::dnsLookup($this->config['ip'], 3, AF_INET6);
                if ($addr) {
                    $this->config['ip'] = $addr;
                }
            }
        }
        $client = new \Swoole\Coroutine\Http\Client($this->config['ip'], intval($this->config['port']));
        $client->set($this->config['options']);
        $path = $this->connPath.'/'.$this->config['tofd'];
        $upgrade = $client->upgrade($path);
        $this->ssh = new Ssh($client);
        if($upgrade && $client->statusCode == 101){
            $this->eventLoop($client);
        }else{
            $client->close();
            $this->log('ws://'.$this->config['ip'].':'.$this->config['port'].$path.' 连接失败');
        }
    }

    protected function eventLoop(Client $client){
        $pingFrame = new \Swoole\WebSocket\Frame();
        $pingFrame->opcode = WEBSOCKET_OPCODE_PING;
        while (true){
            $recv = $client->recv(); // 收到的数据有问题 乱码
            if($recv && $recv->data){
                $this->config['time'] = microtime(true);
                $data = json_decode($recv->data, true);
                if($data){
                    try {
                        $this->message($client, $data);
                    }catch (\Throwable $e){
                        $this->log($recv->data.' => '.$e->getMessage());
                    }
                }
            }else{
                // 发送ping消息 如果发送失败断开
                if($this->config['time'] + 5 < microtime(true)){
                    break;
                }
            }
        }
        $client->close();
        $this->ssh->close();
    }

    /**
     * 收到消息
     * @param array $data
     * @return void
     */
    protected function message(Client $client, array $data){
        $uris = explode('/', $data['uri'] ?? '');
        $toFd =$uris[3] ?? 0;
        switch ($uris[2] ?? ""){
            case "ssh.refresh":
                $this->ssh->refresh($toFd, $data['body']);
                break;
            case "ssh.auth":
                $this->ssh->auth($toFd, $data['body']);
                break;
            case "ssh.command":
                $this->ssh->command($toFd, $data['body']);
                break;
        }
    }

    /**
     * 打印日志
     * @param string $msg
     * @return void
     */
    protected function log(string $msg){
        error_log("[".date('Y-m-d H:i:s')."] ".$msg.PHP_EOL, 3, __DIR__.'/ssh.log');
    }
}
ini_set('date.timezone', 'Asia/Shanghai');
\Swoole\Coroutine\run(function() use ($argv){
    $webClient = new WebClient($argv[1], $argv[2], $argv[3], $argv[4]);
    $webClient->listen();
});
